/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.log4j.lf5.viewer;

import org.apache.log4j.lf5.LogLevel;
import org.apache.log4j.lf5.LogRecord;
import org.apache.log4j.lf5.LogRecordFilter;
import org.apache.log4j.lf5.util.DateFormatManager;
import org.apache.log4j.lf5.util.LogFileParser;
import org.apache.log4j.lf5.util.StreamUtils;
import org.apache.log4j.lf5.viewer.categoryexplorer.CategoryExplorerTree;
import org.apache.log4j.lf5.viewer.categoryexplorer.CategoryPath;
import org.apache.log4j.lf5.viewer.configure.ConfigurationManager;
import org.apache.log4j.lf5.viewer.configure.MRUFileManager;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.List;

/**
 * LogBrokerMonitor .
 * 
 * @author Michael J. Sikorsky
 * @author Robert Shaw
 * @author Brad Marlborough
 * @author Richard Wan
 * @author Brent Sprecher
 * @author Richard Hurst
 */

// Contributed by ThoughtWorks Inc.
public class LogBrokerMonitor {
	//--------------------------------------------------------------------------
	// Constants:
	//--------------------------------------------------------------------------

	public static final String DETAILED_VIEW = "Detailed";
	// public static final String STANDARD_VIEW = "Standard";
	// public static final String COMPACT_VIEW = "Compact";
	//--------------------------------------------------------------------------
	// Protected Variables:
	//--------------------------------------------------------------------------
	protected JFrame _logMonitorFrame;
	protected int _logMonitorFrameWidth = 550;
	protected int _logMonitorFrameHeight = 500;
	protected LogTable _table;
	protected CategoryExplorerTree _categoryExplorerTree;
	protected String _searchText;
	protected String _NDCTextFilter = "";
	protected LogLevel _leastSevereDisplayedLogLevel = LogLevel.DEBUG;

	protected JScrollPane _logTableScrollPane;
	protected JLabel _statusLabel;
	protected Object _lock = new Object();
	protected JComboBox _fontSizeCombo;

	protected int _fontSize = 10;
	protected String _fontName = "Dialog";
	protected String _currentView = DETAILED_VIEW;

	protected boolean _loadSystemFonts = false;
	protected boolean _trackTableScrollPane = true;
	protected Dimension _lastTableViewportSize;
	protected boolean _callSystemExitOnClose = false;
	protected List _displayedLogBrokerProperties = new Vector();

	protected Map _logLevelMenuItems = new HashMap();
	protected Map _logTableColumnMenuItems = new HashMap();

	protected List _levels = null;
	protected List _columns = null;
	protected boolean _isDisposed = false;

	protected ConfigurationManager _configurationManager = null;
	protected MRUFileManager _mruFileManager = null;
	protected File _fileLocation = null;

	//--------------------------------------------------------------------------
	// Private Variables:
	//--------------------------------------------------------------------------

	//--------------------------------------------------------------------------
	// Constructors:
	//--------------------------------------------------------------------------

	/**
	 * Construct a LogBrokerMonitor.
	 */
	public LogBrokerMonitor(List logLevels) {

		_levels = logLevels;
		_columns = LogTableColumn.getLogTableColumns();
		// This allows us to use the LogBroker in command line tools and
		// have the option for it to shutdown.

		String callSystemExitOnClose = System.getProperty("monitor.exit");
		if (callSystemExitOnClose == null) {
			callSystemExitOnClose = "false";
		}
		callSystemExitOnClose = callSystemExitOnClose.trim().toLowerCase();

		if (callSystemExitOnClose.equals("true")) {
			_callSystemExitOnClose = true;
		}

		initComponents();

		_logMonitorFrame.addWindowListener(new LogBrokerMonitorWindowAdaptor(
				this));

	}

	//--------------------------------------------------------------------------
	// Public Methods:
	//--------------------------------------------------------------------------

	/**
	 * Show the frame for the LogBrokerMonitor. Dispatched to the swing thread.
	 */
	public void show(final int delay) {
		if (_logMonitorFrame.isVisible()) {
			return;
		}
		// This request is very low priority, let other threads execute first.
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				Thread.yield();
				pause(delay);
				_logMonitorFrame.setVisible(true);
			}
		});
	}

	public void show() {
		show(0);
	}

	/**
	 * Dispose of the frame for the LogBrokerMonitor.
	 */
	public void dispose() {
		_logMonitorFrame.dispose();
		_isDisposed = true;

		if (_callSystemExitOnClose == true) {
			System.exit(0);
		}
	}

	/**
	 * Hide the frame for the LogBrokerMonitor.
	 */
	public void hide() {
		_logMonitorFrame.setVisible(false);
	}

	/**
	 * Get the DateFormatManager for formatting dates.
	 */
	public DateFormatManager getDateFormatManager() {
		return _table.getDateFormatManager();
	}

	/**
	 * Set the date format manager for formatting dates.
	 */
	public void setDateFormatManager(DateFormatManager dfm) {
		_table.setDateFormatManager(dfm);
	}

	/**
	 * Get the value of whether or not System.exit() will be called when the
	 * LogBrokerMonitor is closed.
	 */
	public boolean getCallSystemExitOnClose() {
		return _callSystemExitOnClose;
	}

	/**
	 * Set the value of whether or not System.exit() will be called when the
	 * LogBrokerMonitor is closed.
	 */
	public void setCallSystemExitOnClose(boolean callSystemExitOnClose) {
		_callSystemExitOnClose = callSystemExitOnClose;
	}

	/**
	 * Add a log record message to be displayed in the LogTable. This method is
	 * thread-safe as it posts requests to the SwingThread rather than
	 * processing directly.
	 */
	public void addMessage(final LogRecord lr) {
		if (_isDisposed == true) {
			// If the frame has been disposed of, do not log any more
			// messages.
			return;
		}

		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				_categoryExplorerTree.getExplorerModel().addLogRecord(lr);
				_table.getFilteredLogTableModel().addLogRecord(lr); // update
																	// table
				updateStatusLabel(); // show updated counts
			}
		});
	}

	public void setMaxNumberOfLogRecords(int maxNumberOfLogRecords) {
		_table.getFilteredLogTableModel().setMaxNumberOfLogRecords(
				maxNumberOfLogRecords);
	}

	public JFrame getBaseFrame() {
		return _logMonitorFrame;
	}

	public void setTitle(String title) {
		_logMonitorFrame.setTitle(title + " - LogFactor5");
	}

	public void setFrameSize(int width, int height) {
		Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
		if (0 < width && width < screen.width) {
			_logMonitorFrameWidth = width;
		}
		if (0 < height && height < screen.height) {
			_logMonitorFrameHeight = height;
		}
		updateFrameSize();
	}

	public void setFontSize(int fontSize) {
		changeFontSizeCombo(_fontSizeCombo, fontSize);
		// setFontSizeSilently(actualFontSize); - changeFontSizeCombo fires
		// event
		// refreshDetailTextArea();
	}

	public void addDisplayedProperty(Object messageLine) {
		_displayedLogBrokerProperties.add(messageLine);
	}

	public Map getLogLevelMenuItems() {
		return _logLevelMenuItems;
	}

	public Map getLogTableColumnMenuItems() {
		return _logTableColumnMenuItems;
	}

	public JCheckBoxMenuItem getTableColumnMenuItem(LogTableColumn column) {
		return getLogTableColumnMenuItem(column);
	}

	public CategoryExplorerTree getCategoryExplorerTree() {
		return _categoryExplorerTree;
	}

	// Added in version 1.2 - gets the value of the NDC text filter
	// This value is set back to null each time the Monitor is initialized.
	public String getNDCTextFilter() {
		return _NDCTextFilter;
	}

	// Added in version 1.2 - sets the NDC Filter based on
	// a String passed in by the user. This value is persisted
	// in the XML Configuration file.
	public void setNDCLogRecordFilter(String textFilter) {
		_table.getFilteredLogTableModel().setLogRecordFilter(
				createNDCLogRecordFilter(textFilter));
	}
	//--------------------------------------------------------------------------
	// Protected Methods:
	//--------------------------------------------------------------------------

	protected void setSearchText(String text) {
		_searchText = text;
	}

	// Added in version 1.2 - Sets the text filter for the NDC
	protected void setNDCTextFilter(String text) {
		// if no value is set, set it to a blank string
		// otherwise use the value provided
		if (text == null) {
			_NDCTextFilter = "";
		} else {
			_NDCTextFilter = text;
		}
	}

	// Added in version 1.2 - Uses a different filter that sorts
	// based on an NDC string passed in by the user. If the string
	// is null or is an empty string, we do nothing.
	protected void sortByNDC() {
		String text = _NDCTextFilter;
		if (text == null || text.length() == 0) {
			return;
		}

		// Use new NDC filter
		_table.getFilteredLogTableModel().setLogRecordFilter(
				createNDCLogRecordFilter(text));
	}

	protected void findSearchText() {
		String text = _searchText;
		if (text == null || text.length() == 0) {
			return;
		}
		int startRow = getFirstSelectedRow();
		int foundRow = findRecord(startRow, text, _table
				.getFilteredLogTableModel().getFilteredRecords());
		selectRow(foundRow);
	}

	protected int getFirstSelectedRow() {
		return _table.getSelectionModel().getMinSelectionIndex();
	}

	protected void selectRow(int foundRow) {
		if (foundRow == -1) {
			String message = _searchText + " not found.";
			JOptionPane.showMessageDialog(_logMonitorFrame, message,
					"Text not found", JOptionPane.INFORMATION_MESSAGE);
			return;
		}
		LF5SwingUtils.selectRow(foundRow, _table, _logTableScrollPane);
	}

	protected int findRecord(int startRow, String searchText, List records) {
		if (startRow < 0) {
			startRow = 0; // start at first element if no rows are selected
		} else {
			startRow++; // start after the first selected row
		}
		int len = records.size();

		for (int i = startRow; i < len; i++) {
			if (matches((LogRecord) records.get(i), searchText)) {
				return i; // found a record
			}
		}
		// wrap around to beginning if when we reach the end with no match
		len = startRow;
		for (int i = 0; i < len; i++) {
			if (matches((LogRecord) records.get(i), searchText)) {
				return i; // found a record
			}
		}
		// nothing found
		return -1;
	}

	/**
	 * Check to see if the any records contain the search string. Searching now
	 * supports NDC messages and date.
	 */
	protected boolean matches(LogRecord record, String text) {
		String message = record.getMessage();
		String NDC = record.getNDC();

		if (message == null && NDC == null || text == null) {
			return false;
		}
		if (message.toLowerCase().indexOf(text.toLowerCase()) == -1
				&& NDC.toLowerCase().indexOf(text.toLowerCase()) == -1) {
			return false;
		}

		return true;
	}

	/**
	 * When the fontsize of a JTextArea is changed, the word-wrapped lines may
	 * become garbled. This method clears and resets the text of the text area.
	 */
	protected void refresh(JTextArea textArea) {
		String text = textArea.getText();
		textArea.setText("");
		textArea.setText(text);
	}

	protected void refreshDetailTextArea() {
		refresh(_table._detailTextArea);
	}

	protected void clearDetailTextArea() {
		_table._detailTextArea.setText("");
	}

	/**
	 * Changes the font selection in the combo box and returns the size actually
	 * selected.
	 * 
	 * @return -1 if unable to select an appropriate font
	 */
	protected int changeFontSizeCombo(JComboBox box, int requestedSize) {
		int len = box.getItemCount();
		int currentValue;
		Object currentObject;
		Object selectedObject = box.getItemAt(0);
		int selectedValue = Integer.parseInt(String.valueOf(selectedObject));
		for (int i = 0; i < len; i++) {
			currentObject = box.getItemAt(i);
			currentValue = Integer.parseInt(String.valueOf(currentObject));
			if (selectedValue < currentValue && currentValue <= requestedSize) {
				selectedValue = currentValue;
				selectedObject = currentObject;
			}
		}
		box.setSelectedItem(selectedObject);
		return selectedValue;
	}

	/**
	 * Does not update gui or cause any events to be fired.
	 */
	protected void setFontSizeSilently(int fontSize) {
		_fontSize = fontSize;
		setFontSize(_table._detailTextArea, fontSize);
		selectRow(0);
		setFontSize(_table, fontSize);
	}

	protected void setFontSize(Component component, int fontSize) {
		Font oldFont = component.getFont();
		Font newFont = new Font(oldFont.getFontName(), oldFont.getStyle(),
				fontSize);
		component.setFont(newFont);
	}

	protected void updateFrameSize() {
		_logMonitorFrame.setSize(_logMonitorFrameWidth, _logMonitorFrameHeight);
		centerFrame(_logMonitorFrame);
	}

	protected void pause(int millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {

		}
	}

	protected void initComponents() {
		//
		// Configure the Frame.
		//
		_logMonitorFrame = new JFrame("LogFactor5");

		_logMonitorFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);

		String resource = "/org/apache/log4j/lf5/viewer/images/lf5_small_icon.gif";
		URL lf5IconURL = getClass().getResource(resource);

		if (lf5IconURL != null) {
			_logMonitorFrame.setIconImage(new ImageIcon(lf5IconURL).getImage());
		}
		updateFrameSize();

		//
		// Configure the LogTable.
		//
		JTextArea detailTA = createDetailTextArea();
		JScrollPane detailTAScrollPane = new JScrollPane(detailTA);
		_table = new LogTable(detailTA);
		setView(_currentView, _table);
		_table.setFont(new Font(_fontName, Font.PLAIN, _fontSize));
		_logTableScrollPane = new JScrollPane(_table);

		if (_trackTableScrollPane) {
			_logTableScrollPane.getVerticalScrollBar().addAdjustmentListener(
					new TrackingAdjustmentListener());
		}

		// Configure the SplitPane between the LogTable & DetailTextArea
		//

		JSplitPane tableViewerSplitPane = new JSplitPane();
		tableViewerSplitPane.setOneTouchExpandable(true);
		tableViewerSplitPane.setOrientation(JSplitPane.VERTICAL_SPLIT);
		tableViewerSplitPane.setLeftComponent(_logTableScrollPane);
		tableViewerSplitPane.setRightComponent(detailTAScrollPane);
		// Make sure to do this last..
		// tableViewerSplitPane.setDividerLocation(1.0); Doesn't work
		// the same under 1.2.x & 1.3
		// "350" is a magic number that provides the correct default
		// behaviour under 1.2.x & 1.3. For example, bumping this
		// number to 400, causes the pane to be completely open in 1.2.x
		// and closed in 1.3
		tableViewerSplitPane.setDividerLocation(350);

		//
		// Configure the CategoryExplorer
		//

		_categoryExplorerTree = new CategoryExplorerTree();

		_table.getFilteredLogTableModel().setLogRecordFilter(
				createLogRecordFilter());

		JScrollPane categoryExplorerTreeScrollPane = new JScrollPane(
				_categoryExplorerTree);
		categoryExplorerTreeScrollPane
				.setPreferredSize(new Dimension(130, 400));

		// Load most recently used file list
		_mruFileManager = new MRUFileManager();

		//
		// Configure the SplitPane between the CategoryExplorer &
		// (LogTable/Detail)
		//

		JSplitPane splitPane = new JSplitPane();
		splitPane.setOneTouchExpandable(true);
		splitPane.setRightComponent(tableViewerSplitPane);
		splitPane.setLeftComponent(categoryExplorerTreeScrollPane);
		// Do this last.
		splitPane.setDividerLocation(130);
		//
		// Add the MenuBar, StatusArea, CategoryExplorer|LogTable to the
		// LogMonitorFrame.
		//
		_logMonitorFrame.getRootPane().setJMenuBar(createMenuBar());
		_logMonitorFrame.getContentPane().add(splitPane, BorderLayout.CENTER);
		_logMonitorFrame.getContentPane().add(createToolBar(),
				BorderLayout.NORTH);
		_logMonitorFrame.getContentPane().add(createStatusArea(),
				BorderLayout.SOUTH);

		makeLogTableListenToCategoryExplorer();
		addTableModelProperties();

		//
		// Configure ConfigurationManager
		//
		_configurationManager = new ConfigurationManager(this, _table);

	}

	protected LogRecordFilter createLogRecordFilter() {
		LogRecordFilter result = new LogRecordFilter() {
			public boolean passes(LogRecord record) {
				CategoryPath path = new CategoryPath(record.getCategory());
				return getMenuItem(record.getLevel()).isSelected()
						&& _categoryExplorerTree.getExplorerModel()
								.isCategoryPathActive(path);
			}
		};
		return result;
	}

	// Added in version 1.2 - Creates a new filter that sorts records based on
	// an NDC string passed in by the user.
	protected LogRecordFilter createNDCLogRecordFilter(String text) {
		_NDCTextFilter = text;
		LogRecordFilter result = new LogRecordFilter() {
			public boolean passes(LogRecord record) {
				String NDC = record.getNDC();
				CategoryPath path = new CategoryPath(record.getCategory());
				if (NDC == null || _NDCTextFilter == null) {
					return false;
				} else if (NDC.toLowerCase().indexOf(
						_NDCTextFilter.toLowerCase()) == -1) {
					return false;
				} else {
					return getMenuItem(record.getLevel()).isSelected()
							&& _categoryExplorerTree.getExplorerModel()
									.isCategoryPathActive(path);
				}
			}
		};

		return result;
	}

	protected void updateStatusLabel() {
		_statusLabel.setText(getRecordsDisplayedMessage());
	}

	protected String getRecordsDisplayedMessage() {
		FilteredLogTableModel model = _table.getFilteredLogTableModel();
		return getStatusText(model.getRowCount(), model.getTotalRowCount());
	}

	protected void addTableModelProperties() {
		final FilteredLogTableModel model = _table.getFilteredLogTableModel();

		addDisplayedProperty(new Object() {
			public String toString() {
				return getRecordsDisplayedMessage();
			}
		});
		addDisplayedProperty(new Object() {
			public String toString() {
				return "Maximum number of displayed LogRecords: "
						+ model._maxNumberOfLogRecords;
			}
		});
	}

	protected String getStatusText(int displayedRows, int totalRows) {
		StringBuffer result = new StringBuffer();
		result.append("Displaying: ");
		result.append(displayedRows);
		result.append(" records out of a total of: ");
		result.append(totalRows);
		result.append(" records.");
		return result.toString();
	}

	protected void makeLogTableListenToCategoryExplorer() {
		ActionListener listener = new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				_table.getFilteredLogTableModel().refresh();
				updateStatusLabel();
			}
		};
		_categoryExplorerTree.getExplorerModel().addActionListener(listener);
	}

	protected JPanel createStatusArea() {
		JPanel statusArea = new JPanel();
		JLabel status = new JLabel("No log records to display.");
		_statusLabel = status;
		status.setHorizontalAlignment(JLabel.LEFT);

		statusArea.setBorder(BorderFactory.createEtchedBorder());
		statusArea.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
		statusArea.add(status);

		return (statusArea);
	}

	protected JTextArea createDetailTextArea() {
		JTextArea detailTA = new JTextArea();
		detailTA.setFont(new Font("Monospaced", Font.PLAIN, 14));
		detailTA.setTabSize(3);
		detailTA.setLineWrap(true);
		detailTA.setWrapStyleWord(false);
		return (detailTA);
	}

	protected JMenuBar createMenuBar() {
		JMenuBar menuBar = new JMenuBar();
		menuBar.add(createFileMenu());
		menuBar.add(createEditMenu());
		menuBar.add(createLogLevelMenu());
		menuBar.add(createViewMenu());
		menuBar.add(createConfigureMenu());
		menuBar.add(createHelpMenu());

		return (menuBar);
	}

	protected JMenu createLogLevelMenu() {
		JMenu result = new JMenu("Log Level");
		result.setMnemonic('l');
		Iterator levels = getLogLevels();
		while (levels.hasNext()) {
			result.add(getMenuItem((LogLevel) levels.next()));
		}

		result.addSeparator();
		result.add(createAllLogLevelsMenuItem());
		result.add(createNoLogLevelsMenuItem());
		result.addSeparator();
		result.add(createLogLevelColorMenu());
		result.add(createResetLogLevelColorMenuItem());

		return result;
	}

	protected JMenuItem createAllLogLevelsMenuItem() {
		JMenuItem result = new JMenuItem("Show all LogLevels");
		result.setMnemonic('s');
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				selectAllLogLevels(true);
				_table.getFilteredLogTableModel().refresh();
				updateStatusLabel();
			}
		});
		return result;
	}

	protected JMenuItem createNoLogLevelsMenuItem() {
		JMenuItem result = new JMenuItem("Hide all LogLevels");
		result.setMnemonic('h');
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				selectAllLogLevels(false);
				_table.getFilteredLogTableModel().refresh();
				updateStatusLabel();
			}
		});
		return result;
	}

	protected JMenu createLogLevelColorMenu() {
		JMenu colorMenu = new JMenu("Configure LogLevel Colors");
		colorMenu.setMnemonic('c');
		Iterator levels = getLogLevels();
		while (levels.hasNext()) {
			colorMenu.add(createSubMenuItem((LogLevel) levels.next()));
		}

		return colorMenu;
	}

	protected JMenuItem createResetLogLevelColorMenuItem() {
		JMenuItem result = new JMenuItem("Reset LogLevel Colors");
		result.setMnemonic('r');
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				// reset the level colors in the map
				LogLevel.resetLogLevelColorMap();

				// refresh the table
				_table.getFilteredLogTableModel().refresh();
			}
		});
		return result;
	}

	protected void selectAllLogLevels(boolean selected) {
		Iterator levels = getLogLevels();
		while (levels.hasNext()) {
			getMenuItem((LogLevel) levels.next()).setSelected(selected);
		}
	}

	protected JCheckBoxMenuItem getMenuItem(LogLevel level) {
		JCheckBoxMenuItem result = (JCheckBoxMenuItem) (_logLevelMenuItems
				.get(level));
		if (result == null) {
			result = createMenuItem(level);
			_logLevelMenuItems.put(level, result);
		}
		return result;
	}

	protected JMenuItem createSubMenuItem(LogLevel level) {
		final JMenuItem result = new JMenuItem(level.toString());
		final LogLevel logLevel = level;
		result.setMnemonic(level.toString().charAt(0));
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				showLogLevelColorChangeDialog(result, logLevel);
			}
		});

		return result;

	}

	protected void showLogLevelColorChangeDialog(JMenuItem result,
			LogLevel level) {
		JMenuItem menuItem = result;
		Color newColor = JColorChooser.showDialog(_logMonitorFrame,
				"Choose LogLevel Color", result.getForeground());

		if (newColor != null) {
			// set the color for the record
			level.setLogLevelColorMap(level, newColor);
			_table.getFilteredLogTableModel().refresh();
		}

	}

	protected JCheckBoxMenuItem createMenuItem(LogLevel level) {
		JCheckBoxMenuItem result = new JCheckBoxMenuItem(level.toString());
		result.setSelected(true);
		result.setMnemonic(level.toString().charAt(0));
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				_table.getFilteredLogTableModel().refresh();
				updateStatusLabel();
			}
		});
		return result;
	}

	// view menu
	protected JMenu createViewMenu() {
		JMenu result = new JMenu("View");
		result.setMnemonic('v');
		Iterator columns = getLogTableColumns();
		while (columns.hasNext()) {
			result.add(getLogTableColumnMenuItem((LogTableColumn) columns
					.next()));
		}

		result.addSeparator();
		result.add(createAllLogTableColumnsMenuItem());
		result.add(createNoLogTableColumnsMenuItem());
		return result;
	}

	protected JCheckBoxMenuItem getLogTableColumnMenuItem(LogTableColumn column) {
		JCheckBoxMenuItem result = (JCheckBoxMenuItem) (_logTableColumnMenuItems
				.get(column));
		if (result == null) {
			result = createLogTableColumnMenuItem(column);
			_logTableColumnMenuItems.put(column, result);
		}
		return result;
	}

	protected JCheckBoxMenuItem createLogTableColumnMenuItem(
			LogTableColumn column) {
		JCheckBoxMenuItem result = new JCheckBoxMenuItem(column.toString());

		result.setSelected(true);
		result.setMnemonic(column.toString().charAt(0));
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				// update list of columns and reset the view
				List selectedColumns = updateView();
				_table.setView(selectedColumns);
			}
		});
		return result;
	}

	protected List updateView() {
		ArrayList updatedList = new ArrayList();
		Iterator columnIterator = _columns.iterator();
		while (columnIterator.hasNext()) {
			LogTableColumn column = (LogTableColumn) columnIterator.next();
			JCheckBoxMenuItem result = getLogTableColumnMenuItem(column);
			// check and see if the checkbox is checked
			if (result.isSelected()) {
				updatedList.add(column);
			}
		}

		return updatedList;
	}

	protected JMenuItem createAllLogTableColumnsMenuItem() {
		JMenuItem result = new JMenuItem("Show all Columns");
		result.setMnemonic('s');
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				selectAllLogTableColumns(true);
				// update list of columns and reset the view
				List selectedColumns = updateView();
				_table.setView(selectedColumns);
			}
		});
		return result;
	}

	protected JMenuItem createNoLogTableColumnsMenuItem() {
		JMenuItem result = new JMenuItem("Hide all Columns");
		result.setMnemonic('h');
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				selectAllLogTableColumns(false);
				// update list of columns and reset the view
				List selectedColumns = updateView();
				_table.setView(selectedColumns);
			}
		});
		return result;
	}

	protected void selectAllLogTableColumns(boolean selected) {
		Iterator columns = getLogTableColumns();
		while (columns.hasNext()) {
			getLogTableColumnMenuItem((LogTableColumn) columns.next())
					.setSelected(selected);
		}
	}

	protected JMenu createFileMenu() {
		JMenu fileMenu = new JMenu("File");
		fileMenu.setMnemonic('f');
		JMenuItem exitMI;
		fileMenu.add(createOpenMI());
		fileMenu.add(createOpenURLMI());
		fileMenu.addSeparator();
		fileMenu.add(createCloseMI());
		createMRUFileListMI(fileMenu);
		fileMenu.addSeparator();
		fileMenu.add(createExitMI());
		return fileMenu;
	}

	/**
	 * Menu item added to allow log files to be opened with the LF5 GUI.
	 */
	protected JMenuItem createOpenMI() {
		JMenuItem result = new JMenuItem("Open...");
		result.setMnemonic('o');
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				requestOpen();
			}
		});
		return result;
	}

	/**
	 * Menu item added to allow log files loaded from a URL to be opened by the
	 * LF5 GUI.
	 */
	protected JMenuItem createOpenURLMI() {
		JMenuItem result = new JMenuItem("Open URL...");
		result.setMnemonic('u');
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				requestOpenURL();
			}
		});
		return result;
	}

	protected JMenuItem createCloseMI() {
		JMenuItem result = new JMenuItem("Close");
		result.setMnemonic('c');
		result.setAccelerator(KeyStroke.getKeyStroke("control Q"));
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				requestClose();
			}
		});
		return result;
	}

	/**
	 * Creates a Most Recently Used file list to be displayed in the File menu
	 */
	protected void createMRUFileListMI(JMenu menu) {

		String[] files = _mruFileManager.getMRUFileList();

		if (files != null) {
			menu.addSeparator();
			for (int i = 0; i < files.length; i++) {
				JMenuItem result = new JMenuItem((i + 1) + " " + files[i]);
				result.setMnemonic(i + 1);
				result.addActionListener(new ActionListener() {
					public void actionPerformed(ActionEvent e) {
						requestOpenMRU(e);
					}
				});
				menu.add(result);
			}
		}
	}

	protected JMenuItem createExitMI() {
		JMenuItem result = new JMenuItem("Exit");
		result.setMnemonic('x');
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				requestExit();
			}
		});
		return result;
	}

	protected JMenu createConfigureMenu() {
		JMenu configureMenu = new JMenu("Configure");
		configureMenu.setMnemonic('c');
		configureMenu.add(createConfigureSave());
		configureMenu.add(createConfigureReset());
		configureMenu.add(createConfigureMaxRecords());

		return configureMenu;
	}

	protected JMenuItem createConfigureSave() {
		JMenuItem result = new JMenuItem("Save");
		result.setMnemonic('s');
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				saveConfiguration();
			}
		});

		return result;
	}

	protected JMenuItem createConfigureReset() {
		JMenuItem result = new JMenuItem("Reset");
		result.setMnemonic('r');
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				resetConfiguration();
			}
		});

		return result;
	}

	protected JMenuItem createConfigureMaxRecords() {
		JMenuItem result = new JMenuItem("Set Max Number of Records");
		result.setMnemonic('m');
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				setMaxRecordConfiguration();
			}
		});

		return result;
	}

	protected void saveConfiguration() {
		_configurationManager.save();
	}

	protected void resetConfiguration() {
		_configurationManager.reset();
	}

	protected void setMaxRecordConfiguration() {
		LogFactor5InputDialog inputDialog = new LogFactor5InputDialog(
				getBaseFrame(), "Set Max Number of Records", "", 10);

		String temp = inputDialog.getText();

		if (temp != null) {
			try {
				setMaxNumberOfLogRecords(Integer.parseInt(temp));
			} catch (NumberFormatException e) {
				LogFactor5ErrorDialog error = new LogFactor5ErrorDialog(
						getBaseFrame(),
						"'"
								+ temp
								+ "' is an invalid parameter.\nPlease try again.");
				setMaxRecordConfiguration();
			}
		}
	}

	protected JMenu createHelpMenu() {
		JMenu helpMenu = new JMenu("Help");
		helpMenu.setMnemonic('h');
		helpMenu.add(createHelpProperties());
		return helpMenu;
	}

	protected JMenuItem createHelpProperties() {
		final String title = "LogFactor5 Properties";
		final JMenuItem result = new JMenuItem(title);
		result.setMnemonic('l');
		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				showPropertiesDialog(title);
			}
		});
		return result;
	}

	protected void showPropertiesDialog(String title) {
		JOptionPane.showMessageDialog(_logMonitorFrame,
				_displayedLogBrokerProperties.toArray(), title,
				JOptionPane.PLAIN_MESSAGE);
	}

	protected JMenu createEditMenu() {
		JMenu editMenu = new JMenu("Edit");
		editMenu.setMnemonic('e');
		editMenu.add(createEditFindMI());
		editMenu.add(createEditFindNextMI());
		editMenu.addSeparator();
		editMenu.add(createEditSortNDCMI());
		editMenu.add(createEditRestoreAllNDCMI());
		return editMenu;
	}

	protected JMenuItem createEditFindNextMI() {
		JMenuItem editFindNextMI = new JMenuItem("Find Next");
		editFindNextMI.setMnemonic('n');
		editFindNextMI.setAccelerator(KeyStroke.getKeyStroke("F3"));
		editFindNextMI.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				findSearchText();
			}
		});
		return editFindNextMI;
	}

	protected JMenuItem createEditFindMI() {
		JMenuItem editFindMI = new JMenuItem("Find");
		editFindMI.setMnemonic('f');
		editFindMI.setAccelerator(KeyStroke.getKeyStroke("control F"));

		editFindMI.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				String inputValue = JOptionPane.showInputDialog(
						_logMonitorFrame, "Find text: ",
						"Search Record Messages", JOptionPane.QUESTION_MESSAGE);
				setSearchText(inputValue);
				findSearchText();
			}
		}

		);
		return editFindMI;
	}

	// Added version 1.2 - Allows users to Sort Log Records by an
	// NDC text filter. A new LogRecordFilter was created to
	// sort the records.
	protected JMenuItem createEditSortNDCMI() {
		JMenuItem editSortNDCMI = new JMenuItem("Sort by NDC");
		editSortNDCMI.setMnemonic('s');
		editSortNDCMI.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				String inputValue = JOptionPane
						.showInputDialog(_logMonitorFrame,
								"Sort by this NDC: ",
								"Sort Log Records by NDC",
								JOptionPane.QUESTION_MESSAGE);
				setNDCTextFilter(inputValue);
				sortByNDC();
				_table.getFilteredLogTableModel().refresh();
				updateStatusLabel();
			}
		}

		);
		return editSortNDCMI;
	}

	// Added in version 1.2 - Resets the LogRecordFilter back to default
	// filter.
	protected JMenuItem createEditRestoreAllNDCMI() {
		JMenuItem editRestoreAllNDCMI = new JMenuItem("Restore all NDCs");
		editRestoreAllNDCMI.setMnemonic('r');
		editRestoreAllNDCMI.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				_table.getFilteredLogTableModel().setLogRecordFilter(
						createLogRecordFilter());
				// reset the text filter
				setNDCTextFilter("");
				_table.getFilteredLogTableModel().refresh();
				updateStatusLabel();
			}
		});
		return editRestoreAllNDCMI;
	}

	protected JToolBar createToolBar() {
		JToolBar tb = new JToolBar();
		tb.putClientProperty("JToolBar.isRollover", Boolean.TRUE);
		JComboBox fontCombo = new JComboBox();
		JComboBox fontSizeCombo = new JComboBox();
		_fontSizeCombo = fontSizeCombo;

		ClassLoader cl = this.getClass().getClassLoader();
		if (cl == null) {
			cl = ClassLoader.getSystemClassLoader();
		}
		URL newIconURL = cl.getResource("org/apache/log4j/lf5/viewer/"
				+ "images/channelexplorer_new.gif");

		ImageIcon newIcon = null;

		if (newIconURL != null) {
			newIcon = new ImageIcon(newIconURL);
		}

		JButton newButton = new JButton("Clear Log Table");

		if (newIcon != null) {
			newButton.setIcon(newIcon);
		}

		newButton.setToolTipText("Clear Log Table.");
		// newButton.setBorder(BorderFactory.createEtchedBorder());

		newButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				_table.clearLogRecords();
				_categoryExplorerTree.getExplorerModel().resetAllNodeCounts();
				updateStatusLabel();
				clearDetailTextArea();
				LogRecord.resetSequenceNumber();
			}
		});

		Toolkit tk = Toolkit.getDefaultToolkit();
		// This will actually grab all the fonts

		String[] fonts;

		if (_loadSystemFonts) {
			fonts = GraphicsEnvironment.getLocalGraphicsEnvironment()
					.getAvailableFontFamilyNames();
		} else {
			fonts = tk.getFontList();
		}

		for (int j = 0; j < fonts.length; j++) {
			fontCombo.addItem(fonts[j]);
		}

		fontCombo.setSelectedItem(_fontName);

		fontCombo.addActionListener(

		new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				JComboBox box = (JComboBox) e.getSource();
				String font = (String) box.getSelectedItem();
				_table.setFont(new Font(font, Font.PLAIN, _fontSize));
				_fontName = font;
			}
		});

		fontSizeCombo.addItem("8");
		fontSizeCombo.addItem("9");
		fontSizeCombo.addItem("10");
		fontSizeCombo.addItem("12");
		fontSizeCombo.addItem("14");
		fontSizeCombo.addItem("16");
		fontSizeCombo.addItem("18");
		fontSizeCombo.addItem("24");

		fontSizeCombo.setSelectedItem(String.valueOf(_fontSize));
		fontSizeCombo.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				JComboBox box = (JComboBox) e.getSource();
				String size = (String) box.getSelectedItem();
				int s = Integer.valueOf(size).intValue();

				setFontSizeSilently(s);
				refreshDetailTextArea();
				_fontSize = s;
			}
		});

		tb.add(new JLabel(" Font: "));
		tb.add(fontCombo);
		tb.add(fontSizeCombo);
		tb.addSeparator();
		tb.addSeparator();
		tb.add(newButton);

		newButton.setAlignmentY(0.5f);
		newButton.setAlignmentX(0.5f);

		fontCombo.setMaximumSize(fontCombo.getPreferredSize());
		fontSizeCombo.setMaximumSize(fontSizeCombo.getPreferredSize());

		return (tb);
	}

	// protected void setView(String viewString, LogTable table) {
	// if (STANDARD_VIEW.equals(viewString)) {
	// table.setStandardView();
	// } else if (COMPACT_VIEW.equals(viewString)) {
	// table.setCompactView();
	// } else if (DETAILED_VIEW.equals(viewString)) {
	// table.setDetailedView();
	// } else {
	// String message = viewString + "does not match a supported view.";
	// throw new IllegalArgumentException(message);
	// }
	// _currentView = viewString;
	// }

	protected void setView(String viewString, LogTable table) {
		if (DETAILED_VIEW.equals(viewString)) {
			table.setDetailedView();
		} else {
			String message = viewString + "does not match a supported view.";
			throw new IllegalArgumentException(message);
		}
		_currentView = viewString;
	}

	protected JComboBox createLogLevelCombo() {
		JComboBox result = new JComboBox();
		Iterator levels = getLogLevels();
		while (levels.hasNext()) {
			result.addItem(levels.next());
		}
		result.setSelectedItem(_leastSevereDisplayedLogLevel);

		result.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				JComboBox box = (JComboBox) e.getSource();
				LogLevel level = (LogLevel) box.getSelectedItem();
				setLeastSevereDisplayedLogLevel(level);
			}
		});
		result.setMaximumSize(result.getPreferredSize());
		return result;
	}

	protected void setLeastSevereDisplayedLogLevel(LogLevel level) {
		if (level == null || _leastSevereDisplayedLogLevel == level) {
			return; // nothing to do
		}
		_leastSevereDisplayedLogLevel = level;
		_table.getFilteredLogTableModel().refresh();
		updateStatusLabel();
	}

	/**
	 * Ensures that the Table's ScrollPane Viewport will "track" with updates to
	 * the Table. When the vertical scroll bar is at its bottom anchor and
	 * tracking is enabled then viewport will stay at the bottom most point of
	 * the component. The purpose of this feature is to allow a developer to
	 * watch the table as messages arrive and not have to scroll after each new
	 * message arrives. When the vertical scroll bar is at any other location,
	 * then no tracking will happen.
	 * 
	 * @deprecated tracking is now done automatically.
	 */
	protected void trackTableScrollPane() {
		// do nothing
	}

	protected void centerFrame(JFrame frame) {
		Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
		Dimension comp = frame.getSize();

		frame.setLocation(((screen.width - comp.width) / 2),
				((screen.height - comp.height) / 2));

	}

	/**
	 * Uses a JFileChooser to select a file to opened with the LF5 GUI.
	 */
	protected void requestOpen() {
		JFileChooser chooser;

		if (_fileLocation == null) {
			chooser = new JFileChooser();
		} else {
			chooser = new JFileChooser(_fileLocation);
		}

		int returnVal = chooser.showOpenDialog(_logMonitorFrame);
		if (returnVal == JFileChooser.APPROVE_OPTION) {
			File f = chooser.getSelectedFile();
			if (loadLogFile(f)) {
				_fileLocation = chooser.getSelectedFile();
				_mruFileManager.set(f);
				updateMRUList();
			}
		}
	}

	/**
	 * Uses a Dialog box to accept a URL to a file to be opened with the LF5
	 * GUI.
	 */
	protected void requestOpenURL() {
		LogFactor5InputDialog inputDialog = new LogFactor5InputDialog(
				getBaseFrame(), "Open URL", "URL:");
		String temp = inputDialog.getText();

		if (temp != null) {
			if (temp.indexOf("://") == -1) {
				temp = "http://" + temp;
			}

			try {
				URL url = new URL(temp);
				if (loadLogFile(url)) {
					_mruFileManager.set(url);
					updateMRUList();
				}
			} catch (MalformedURLException e) {
				LogFactor5ErrorDialog error = new LogFactor5ErrorDialog(
						getBaseFrame(), "Error reading URL.");
			}
		}
	}

	/**
	 * Removes old file list and creates a new file list with the updated MRU
	 * list.
	 */
	protected void updateMRUList() {
		JMenu menu = _logMonitorFrame.getJMenuBar().getMenu(0);
		menu.removeAll();
		menu.add(createOpenMI());
		menu.add(createOpenURLMI());
		menu.addSeparator();
		menu.add(createCloseMI());
		createMRUFileListMI(menu);
		menu.addSeparator();
		menu.add(createExitMI());
	}

	protected void requestClose() {
		setCallSystemExitOnClose(false);
		closeAfterConfirm();
	}

	/**
	 * Opens a file in the MRU list.
	 */
	protected void requestOpenMRU(ActionEvent e) {
		String file = e.getActionCommand();
		StringTokenizer st = new StringTokenizer(file);
		String num = st.nextToken().trim();
		file = st.nextToken("\n");

		try {
			int index = Integer.parseInt(num) - 1;

			InputStream in = _mruFileManager.getInputStream(index);
			LogFileParser lfp = new LogFileParser(in);
			lfp.parse(this);

			_mruFileManager.moveToTop(index);
			updateMRUList();

		} catch (Exception me) {
			LogFactor5ErrorDialog error = new LogFactor5ErrorDialog(
					getBaseFrame(), "Unable to load file " + file);
		}

	}

	protected void requestExit() {
		_mruFileManager.save();
		setCallSystemExitOnClose(true);
		closeAfterConfirm();
	}

	protected void closeAfterConfirm() {
		StringBuffer message = new StringBuffer();

		if (_callSystemExitOnClose == false) {
			message.append("Are you sure you want to close the logging ");
			message.append("console?\n");
			message
					.append("(Note: This will not shut down the Virtual Machine,\n");
			message.append("or the Swing event thread.)");
		} else {
			message.append("Are you sure you want to exit?\n");
			message.append("This will shut down the Virtual Machine.\n");
		}

		String title = "Are you sure you want to dispose of the Logging Console?";

		if (_callSystemExitOnClose == true) {
			title = "Are you sure you want to exit?";
		}
		int value = JOptionPane.showConfirmDialog(_logMonitorFrame, message
				.toString(), title, JOptionPane.OK_CANCEL_OPTION,
				JOptionPane.QUESTION_MESSAGE, null);

		if (value == JOptionPane.OK_OPTION) {
			dispose();
		}
	}

	protected Iterator getLogLevels() {
		return _levels.iterator();
	}

	protected Iterator getLogTableColumns() {
		return _columns.iterator();
	}

	/**
	 * Loads and parses a log file.
	 */
	protected boolean loadLogFile(File file) {
		boolean ok = false;
		try {
			LogFileParser lfp = new LogFileParser(file);
			lfp.parse(this);
			ok = true;
		} catch (IOException e) {
			LogFactor5ErrorDialog error = new LogFactor5ErrorDialog(
					getBaseFrame(), "Error reading " + file.getName());
		}

		return ok;
	}

	/**
	 * Loads a parses a log file running on a server.
	 */
	protected boolean loadLogFile(URL url) {
		boolean ok = false;
		try {
			LogFileParser lfp = new LogFileParser(url.openStream());
			lfp.parse(this);
			ok = true;
		} catch (IOException e) {
			LogFactor5ErrorDialog error = new LogFactor5ErrorDialog(
					getBaseFrame(), "Error reading URL:" + url.getFile());
		}
		return ok;
	}
	//--------------------------------------------------------------------------
	// Private Methods:
	//--------------------------------------------------------------------------

	//--------------------------------------------------------------------------
	// Nested Top-Level Classes or Interfaces:
	//--------------------------------------------------------------------------

	class LogBrokerMonitorWindowAdaptor extends WindowAdapter {
		protected LogBrokerMonitor _monitor;

		public LogBrokerMonitorWindowAdaptor(LogBrokerMonitor monitor) {
			_monitor = monitor;
		}

		public void windowClosing(WindowEvent ev) {
			_monitor.requestClose();
		}
	}
}
